package liquidfs.data.ops;

import java.io.*;
import java.util.*;

import com.thoughtworks.xstream.annotations.*;

import utility.*;

import liquidfs.*;
import liquidfs.data.*;

@XStreamAlias("queued-operation")
public class BufferedOperation implements Operation<Object> {

	private Operation<?> operation;
	
	private byte[] buffer;
	private int size;
	
	/**
	 * A BufferedOperation is a wrapper around an ordinary operation that saves its input data into an internal buffer so that it can be executed at a later time without keeping the stream open. Note that the input stream
	 * passed to the BufferedOperation when invoking the @{link execute} method is ignored in favour of the internal buffer.
	 * @param operation The operation to execute
	 * @param in The input stream to save and pass to this operation later
	 */
	public BufferedOperation(Operation<?> operation, InputStream in) {
		
	   // Create the internal buffer
		boolean success = this.save(operation, in);
		
		if (!success)
		   throw new UserUncheckedException("Unable to save input stream for operation " + operation.getID() + " (" + operation.getClass() + ")");
		
		this.operation = operation;
	}
	
	public int getSize () {
	   return this.size;
	}
	
	protected boolean save (Operation<?> operation, InputStream in) {
	   
	   if (in == null) {
	      this.buffer = null;
	      this.size = 0;
	      return true;
	   }
	   
	   this.buffer = new byte[operation.getMaximumInputSize()];
	   
	   int read;
      int total = 0;
      
      try {
         
         // Read the input stream and save it to the buffer
         do {
            read = in.read(buffer, total, buffer.length - total);
            
            // If we've reached the end of the stream, stop reading
            if (read == -1)
               break;
            
            // Keep track of how many bytes we have
            total += read;
         }
         while (total < buffer.length);
         
         // If we've read up to capacity, check whether or not this is everything
         if (total == buffer.length && in.read() != -1) {
            // If it's too big, we can't save it
            return false;
         }
         
         // Save the total number of bytes we read
         this.size = total;
         return true;
         
      }
      catch (IOException e) {
         // If we couldn't save it, return false
         return false;
      }
      
	}
	
	@Override
	public PeerDevice getOrigin () {
	   return operation.getOrigin();
	}
	
	@Override
   public boolean isModification() {
      return operation.isModification();
   }
	
	@Override
	public boolean isFileOperation() {
	   return operation.isFileOperation();
	}
	
	public Operation<?> getInternalOperation () {
		return this.operation;
	}
	
	public InputStream getInputStream () {
	   return new ByteBufferInputStream(this.buffer, this.size);
	}
	
	public boolean reached (PeerDevice device) {
	   return this.operation.reached(device);
	}
	
	public void addReachedDevice (PeerDevice device) {
	   this.operation.addReachedDevice(device);
	}
	
	public void addReachedDevices (Collection<PeerDevice> devices) {
	   this.operation.addReachedDevices(devices);
	}
	
	public Set<PeerDevice> getReachedDevices () {
	   return this.operation.getReachedDevices();
	}

	@Override
	public Object execute(Application app, InputStream in, OutputStream out) {
		return operation.execute(app, this.getInputStream(), out);
	}

   @Override
   public Notification getNotification() {
      return operation.getNotification();
   }

   @Override
   public int getMaximumInputSize() {
      return operation.getMaximumInputSize();
   }

   @Override
   public int getMaximumOutputSize() {
      return operation.getMaximumOutputSize();
   }
   
   @Override
   public long getID () {
      return operation.getID();
   }

}
